/*
 * Copyright 2010, 2011, 2012 mapsforge.org
 *
 * This program is free software: you can redistribute it and/or modify it under the
 * terms of the GNU Lesser General Public License as published by the Free Software
 * Foundation, either version 3 of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT ANY
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
 * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License along with
 * this program. If not, see <http://www.gnu.org/licenses/>.
 */
package org.mapsforge.android.maps;

import java.io.OutputStream;

import org.mapsforge.core.GeoPoint;
import org.mapsforge.core.MapPosition;
import org.mapsforge.core.MercatorProjection;
import org.mapsforge.core.Tile;

import android.graphics.Bitmap;
import android.graphics.Bitmap.CompressFormat;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Matrix;

/**
 * A FrameBuffer uses two separate memory buffers to display the current and build up the next frame.
 */
public class FrameBuffer
{
    static final int MAP_VIEW_BACKGROUND = Color.rgb(238, 238, 238);

    private int height;
    private final MapView mapView;
    private Bitmap mapViewBitmap1;
    private Bitmap mapViewBitmap2;
    private Canvas mapViewCanvas;
    private final Matrix matrix;
    private int width;

    FrameBuffer(MapView mapView)
    {
        this.mapView = mapView;
        this.mapViewCanvas = new Canvas();
        this.matrix = new Matrix();
    }

    /**
     * Draws a tile bitmap at the right position on the MapView bitmap.
     * 
     * @param tile
     *            the corresponding tile for the bitmap.
     * @param bitmap
     *            the bitmap to be drawn.
     * @return true if the tile is visible and the bitmap was drawn, false otherwise.
     */
    public synchronized boolean drawBitmap(Tile tile, Bitmap bitmap)
    {
        MapPosition mapPosition = this.mapView.getMapPosition().getMapPosition();
        if (tile.zoomLevel != mapPosition.zoomLevel)
        {
            // the tile doesn't fit to the current zoom level
            return false;
        }
        else
            if (this.mapView.isZoomAnimatorRunning())
            {
                // do not disturb the ongoing animation
                return false;
            }

        GeoPoint geoPoint = mapPosition.geoPoint;
        double pixelLeft = MercatorProjection.longitudeToPixelX(geoPoint.getLongitude(), mapPosition.zoomLevel);
        double pixelTop = MercatorProjection.latitudeToPixelY(geoPoint.getLatitude(), mapPosition.zoomLevel);
        pixelLeft -= this.width >> 1;
        pixelTop -= this.height >> 1;

        if (pixelLeft - tile.getPixelX() > Tile.TILE_SIZE || pixelLeft + this.width < tile.getPixelX())
        {
            // no horizontal intersection
            return false;
        }
        else
            if (pixelTop - tile.getPixelY() > Tile.TILE_SIZE || pixelTop + this.height < tile.getPixelY())
            {
                // no vertical intersection
                return false;
            }

        if (!this.matrix.isIdentity())
        {
            // change the current MapView bitmap
            this.mapViewBitmap2.eraseColor(MAP_VIEW_BACKGROUND);
            this.mapViewCanvas.setBitmap(this.mapViewBitmap2);

            // draw the previous MapView bitmap on the current MapView bitmap
            this.mapViewCanvas.drawBitmap(this.mapViewBitmap1, this.matrix, null);
            this.matrix.reset();

            // swap the two MapView bitmaps
            Bitmap mapViewBitmapSwap = this.mapViewBitmap1;
            this.mapViewBitmap1 = this.mapViewBitmap2;
            this.mapViewBitmap2 = mapViewBitmapSwap;
        }

        // draw the tile bitmap at the correct position
        float left = (float) (tile.getPixelX() - pixelLeft);
        float top = (float) (tile.getPixelY() - pixelTop);
        this.mapViewCanvas.drawBitmap(bitmap, left, top, null);
        return true;
    }

    /**
     * Scales the matrix of the MapView and all its overlays.
     * 
     * @param scaleX
     *            the horizontal scale.
     * @param scaleY
     *            the vertical scale.
     * @param pivotX
     *            the horizontal pivot point.
     * @param pivotY
     *            the vertical pivot point.
     */
    public void matrixPostScale(float scaleX, float scaleY, float pivotX, float pivotY)
    {
        synchronized (this)
        {
            this.matrix.postScale(scaleX, scaleY, pivotX, pivotY);
            synchronized (this.mapView.getOverlays())
            {
                for (int i = 0, n = this.mapView.getOverlays().size(); i < n; ++i)
                {
                    this.mapView.getOverlays().get(i).matrixPostScale(scaleX, scaleY, pivotX, pivotY);
                }
            }
        }
    }

    /**
     * Translates the matrix of the MapView and all its overlays.
     * 
     * @param translateX
     *            the horizontal translation.
     * @param translateY
     *            the vertical translation.
     */
    public void matrixPostTranslate(float translateX, float translateY)
    {
        synchronized (this)
        {
            this.matrix.postTranslate(translateX, translateY);
            synchronized (this.mapView.getOverlays())
            {
                for (int i = 0, n = this.mapView.getOverlays().size(); i < n; ++i)
                {
                    this.mapView.getOverlays().get(i).matrixPostTranslate(translateX, translateY);
                }
            }
        }
    }

    synchronized void clear()
    {
        if (this.mapViewBitmap1 != null)
        {
            this.mapViewBitmap1.eraseColor(MAP_VIEW_BACKGROUND);
        }

        if (this.mapViewBitmap2 != null)
        {
            this.mapViewBitmap2.eraseColor(MAP_VIEW_BACKGROUND);
        }
    }

    synchronized boolean compress(CompressFormat compressFormat, int quality, OutputStream outputStream)
    {
        if (this.mapViewBitmap1 == null)
        {
            return false;
        }

        return this.mapViewBitmap1.compress(compressFormat, quality, outputStream);
    }

    synchronized void destroy()
    {
        if (this.mapViewBitmap1 != null)
        {
            this.mapViewBitmap1.recycle();
        }

        if (this.mapViewBitmap2 != null)
        {
            this.mapViewBitmap2.recycle();
        }
        this.mapViewCanvas = null;
    }

    public synchronized void draw(Canvas canvas)
    {
        if (this.mapViewBitmap1 != null)
        {
            canvas.drawBitmap(this.mapViewBitmap1, this.matrix, null);
        }
    }

    synchronized void onSizeChanged()
    {
        this.destroy();
        this.mapViewCanvas = new Canvas();
        this.width = this.mapView.getWidth();
        this.height = this.mapView.getHeight();
        this.mapViewBitmap1 = Bitmap.createBitmap(this.width, this.height, Bitmap.Config.RGB_565);
        this.mapViewBitmap2 = Bitmap.createBitmap(this.width, this.height, Bitmap.Config.RGB_565);
        clear();
        this.mapViewCanvas.setBitmap(this.mapViewBitmap1);
    }
}
